import * as rabbitMq from 'amqplib'
import {Channel, Connection} from 'amqplib'
import * as Bluebird from "bluebird"
import * as redis from 'redis'
import Database from './database/database'
import {PlayerModel as Player} from "./database/models/player"
import {GameNames} from "./gameName";
import {GameTypes} from "./match/gameTypes"
import {IMessageEmitter, IMessageGroupEmitter, Message, toBuffer} from "./match/messageBus";
import {RabbitQueueMessageSource} from "./match/messageBus/RabbitQueueSource";
import {RoomRegister} from "./match/RoomRegister"
import RoomProxy, {recoverFunc} from "./match/roomRmqProxy"
import {Tournament, TournamentConfig} from "./match/Tournament"
import {PlayerRmqProxy} from "./player/PlayerRmqProxy"
import config from "./utils/config"
import {logger} from "./utils/logger";

Bluebird.promisifyAll(redis.RedisClient.prototype)

const alwaysOk = () => true

class BackendProcess {

  dataBaseUrl: string
  rabbitMqServer: string
  gameName: GameTypes
  cluster: string
  private redisClient: any
  private lobbyChannel: Channel
  private connection: Connection
  private lobby: any
  private roomRegister: RoomRegister
  roomRecover: recoverFunc

  constructor({dataBaseUrl, rabbitMqServer, gameName, cluster, redisClient, Lobby}) {
    this.dataBaseUrl = dataBaseUrl
    this.rabbitMqServer = rabbitMqServer
    this.gameName = gameName
    this.cluster = cluster
    this.redisClient = redisClient
    this.lobby = Lobby
    this.roomRegister = new RoomRegister(redisClient)
  }

  recoverPolicy: (any) => boolean = (roomJson: any) => roomJson


  getPlayerModel = async (playerId: string): Promise<any> => {

    let playerModel
    if (playerId.startsWith('robot')) {
      try {
        playerModel = {
          ...JSON.parse(await this.redisClient.rpoplpushAsync('profiles', 'profiles')),
          gold: 100,
          _id: playerId
        }
      } catch (e) {
        playerModel = {
          _id: 'robot',
          name: '无名氏',
          headImgUrl: 'http://cdn2.51ulong.com/avatar/head_5.png'
        }
      }
    } else {
      playerModel = await Player.findOne({_id: playerId}).lean().exec()
    }

    return playerModel;
  }

  async getRoomIdsToRecover(): Promise<string[]> {
    const roomIds: string[] = await this.redisClient.smembersAsync(`cluster-${this.cluster}`)
    return roomIds
  }

  sendMessage(name: string, message: any, playerRouteKey: string) {
    this.lobbyChannel.publish('userCenter', playerRouteKey, toBuffer({payload: message, name}))
  }

  async execute() {
    await Database.connect(this.dataBaseUrl)
    this.connection = await rabbitMq.connect(this.rabbitMqServer)

    this.lobbyChannel = await this.connection.createChannel()

    const lobbyQueueName = `${this.gameName}Lobby`
    const tournamentQueueName = `${this.gameName}Tournament`
    await this.lobbyChannel.assertQueue(lobbyQueueName, {durable: false})
    await this.lobbyChannel.assertQueue(tournamentQueueName, {durable: false})
    await this.lobbyChannel.assertExchange('exGameCenter', 'topic', {durable: false})
    await this.lobbyChannel.assertExchange('exTournament', 'topic', {durable: false})
    await this.lobbyChannel.assertExchange('exClubCenter', 'topic', {durable: false})
    await this.lobbyChannel.assertExchange('userCenter', 'topic', {durable: false})

    this.lobby.clubBroadcaster = {
      broadcast: async (clubId: string) => {
        // const rooms = this.lobby.getClubRooms(clubId)
        // const clubInfo = await getClubInfo(clubId)
        //
        // await requestToAllClubMember(this.lobbyChannel, 'newClubRoomCreated', clubId, this.gameName, {
        //   ok: true,
        //   roomInfo: rooms,
        //   ...clubInfo
        // });
      },

      updateClubRoomInfo: async (clubId: string, roomInfo: { roomNum: string, capacity: number, current: number }) => {
        // await requestToAllClubMember(this.lobbyChannel, 'club/updateClubRoom', clubId, this.gameName, roomInfo);
      }

    }

    const roomIds: string[] = await this.getRoomIdsToRecover()

    await this.recoverRooms(roomIds)
    await this.lobbyChannel.consume(lobbyQueueName, async (message) => {
      const messageBody = JSON.parse(message.content.toString())
      const playerRouteKey = `user.${messageBody.from}.${this.gameName}`

      const unfinishedRoomId = await this.lobby.getDisconnectedRoom(messageBody.from)
      if (unfinishedRoomId) {
        this.sendMessage('room/join-fail', {reason: `无法创建房间,还有未结束的房间${unfinishedRoomId}`}, playerRouteKey);
        return
      }

      const playerModel = await this.getPlayerModel(messageBody.from)
      if (playerModel) {
        const alreadyInRoom = await this.roomRegister.roomNumber(playerModel._id, this.gameName)
        if (alreadyInRoom) {
          this.sendMessage('room/join-fail', {reason: `您还有一个房间未打完 ${alreadyInRoom}`}, playerRouteKey);
          return
        }

        if (messageBody.payload.clubId) {
          await this.createPrivateClubRoom(playerModel, messageBody, messageBody.payload.clubId)
        } else {
          await this.createPrivateRoom(playerModel, messageBody)
        }
      } else {
        this.sendMessage('room/join-fail', {reason: `无法创建房间,非法用户信息`}, playerRouteKey);
      }
    }, {noAck: true})


    await this.lobbyChannel.consume(tournamentQueueName, async (message) => {

      const messageBody = JSON.parse(message.content.toString())

      if (messageBody.name === 'startTournament') {

        const {tournamentId, players, config} = messageBody.payload
        try {
          config.tournamentId = tournamentId
          await this.startTournament(config, players.map(pId => ({_id: pId, score: 0})))
        } catch (e) {
          console.log(`${__filename}:169 \n`, e);
        }
      }

      if (messageBody.name === 'startBattle') {

        const {tournamentId, players, config} = messageBody.payload
        try {
          config.tournamentId = tournamentId
          await this.startBattle(config, players.map(pId => ({_id: pId, score: 0})))
        } catch (e) {
          console.log(`${__filename}:169 \n`, e);
        }
      }

    }, {noAck: true})

    return
  }

  async startBattle(tc: TournamentConfig, players) {
    const roomId = await this.redisClient.lpopAsync('roomIds')
    const room = await this.lobby.createBattleRoom(roomId, tc.rule, players)

    try {

      await this.attachRoomToBackendTopic(room)
      await this.pullPlayerIntoRoom(players, roomId)
    } catch (e) {
      logger.error('startBattleRoom error', e)
    }
  }

  async pullPlayerIntoRoom(players, roomId) {
    for (const p of players) {
      this.lobbyChannel.publish('exGameCenter', `${this.gameName}.${roomId}`, toBuffer({
        name: 'joinRoom',
        payload: {},
        from: p._id,
        ip: 'local'
      }))
    }
  }

  private async attachRoomToBackendTopic(room) {

    const gameChannel = await this.connection.createChannel()
    const roomQueueReply = await gameChannel.assertQueue(`${this.gameName}.${room._id}`, {
      durable: false,
      autoDelete: true
    })
    await gameChannel.bindQueue(roomQueueReply.queue, 'exGameCenter', `${this.gameName}.${room._id}`)

    const roomProxy = new RoomProxy(room,
      {
        redisClient: this.redisClient, gameChannel: gameChannel, gameQueue: roomQueueReply,
        cluster: this.cluster, userCenter: {getPlayerModel: this.getPlayerModel}
      }, this.gameName)

    await this.redisClient.saddAsync('room', room._id)
    await this.redisClient.saddAsync(`cluster-${this.cluster}`, room._id)
    await this.redisClient.setAsync('room:info:' + room._id, JSON.stringify(room.toJSON()))
  }

  async startTournament(tc: TournamentConfig, players) {

    class RabbitPlayerGroupBroadcaster implements IMessageGroupEmitter {
      private channel: Channel;

      constructor(channel: Channel) {
        this.channel = channel
      }

      close(): void {
      }

      emit(message: Message, playerIds) {
        try {

          playerIds.forEach(id => {
            this.channel.publish('userCenter', `user.${id}`, toBuffer(message))
          })

          console.log('=====>', JSON.stringify(message));
          // this.channel.publish('exTournament', this.routeKey, toBuffer(message))
        } catch (e) {

        }
      }
    }

    class RabbitQueueEmitter implements IMessageEmitter {
      constructor(readonly channel: Channel, readonly queueName: string) {
      }

      close(): void {
      }

      emit(message: Message) {
        try {

          this.channel.sendToQueue(this.queueName, toBuffer(message))
        } catch (e) {

        }
      }
    }


    const channel: Channel = await this.connection.createChannel();

    const source = new RabbitQueueMessageSource(`tournament.${tc._id}`, channel)
    const roomReporter = new RabbitQueueEmitter(channel, `tournament.${tc._id}`)
    const emitter = new RabbitPlayerGroupBroadcaster(channel)


    const tournament = new Tournament(tc, players, {
      source,
      emitter, roomReport: roomReporter
    }, {startTournamentRoom: this.startTournamentRoom.bind(this)})

    await tournament.start()


  }


  async startTournamentRoom(players, config: TournamentConfig, roomReporter: IMessageEmitter) {


    const roomId = await this.redisClient.lpopAsync('roomIds')

    const room = this.lobby.createTournamentRoom(roomId, config.rule, players, roomReporter)

    try {
      const gameChannel = await this.connection.createChannel()
      const roomQueueReply = await gameChannel.assertQueue(`${this.gameName}.${room._id}`, {
        durable: false,
        autoDelete: true
      })
      await gameChannel.bindQueue(roomQueueReply.queue, 'exGameCenter', `${this.gameName}.${room._id}`)

      const roomProxy = new RoomProxy(room,
        {
          redisClient: this.redisClient, gameChannel: gameChannel, gameQueue: roomQueueReply,
          cluster: this.cluster, userCenter: {getPlayerModel: this.getPlayerModel}
        }, this.gameName)

      await this.redisClient.saddAsync('room', room._id)
      await this.redisClient.saddAsync(`cluster-${this.cluster}`, room._id)
      await this.redisClient.setAsync('room:info:' + room._id, JSON.stringify(room.toJSON()))

      for (const p of players) {
        this.lobbyChannel.publish('exGameCenter', `${this.gameName}.${roomId}`, toBuffer({
          name: 'joinRoom',
          payload: {},
          from: p._id,
          ip: 'local'
        }))
      }

    } catch (e) {
      logger.error('create room error', e)
    }

    return roomId
  }

  async createPrivateRoom(playerModel, messageBody) {
    const playerRouteKey = `user.${messageBody.from}.${this.gameName}`

    const fee = this.lobby.roomFee(messageBody.payload.rule)

    if (playerModel.gem < fee) {
      this.sendMessage('room/join-fail', {reason: `房卡不足请充值(需要房费${fee})`}, playerRouteKey);
      return
    }

    const roomId = await this.redisClient.lpopAsync('roomIds')
    if (!roomId) {
      this.sendMessage('room/join-fail', {reason: `服务器错误,无法创建房间 [-9]`}, playerRouteKey);
      return
    }

    const room = await this.lobby.createRoom(false, Number(roomId), messageBody.payload.rule)
    room.ownerId = messageBody.from
    try {
      const gameChannel = await this.connection.createChannel()
      const roomQueueReply = await gameChannel.assertQueue(`${this.gameName}.${room._id}`, {
        durable: false,
        autoDelete: true
      })
      await gameChannel.bindQueue(roomQueueReply.queue, 'exGameCenter', `${this.gameName}.${room._id}`)

      const roomProxy = new RoomProxy(room,
        {
          redisClient: this.redisClient, gameChannel: gameChannel, gameQueue: roomQueueReply,
          cluster: this.cluster, userCenter: {getPlayerModel: this.getPlayerModel}
        }, this.gameName)

      let theCreator = new PlayerRmqProxy(
        {...playerModel, _id: messageBody.from, ip: messageBody.ip},
        gameChannel,
        this.gameName
      )

      theCreator.sendMessage('room/join-success', {_id: room._id, rule: room.rule})
      roomProxy.joinAsCreator(theCreator)

      await this.redisClient.saddAsync('room', room._id)
      await this.roomRegister.putPlayerInGameRoom(messageBody.from, this.gameName, room._id)

      await this.redisClient.saddAsync(`cluster-${this.cluster}`, room._id)
      await this.redisClient.setAsync('room:info:' + room._id, JSON.stringify(room.toJSON()))
    } catch (e) {
      logger.error('create room error', e)
    }
  }

  async createPrivateClubRoom(playerModel, messageBody, clubId) {
    const playerRouteKey = `user.${messageBody.from}.${this.gameName}`

    const clubOwner = await this.lobby.getClubOwner(clubId)
    if (!clubOwner) {
      this.sendMessage('room/join-fail', {reason: '亲友圈参数错误'}, playerRouteKey);
      return
    }

    const clubOwnerSocket = new PlayerRmqProxy(clubOwner, this.lobbyChannel, this.gameName)


    if (messageBody.payload.rule.clubPersonalRoom === false) {
      if (clubOwner.gem < this.lobby.roomFee(messageBody.payload.rule)) {
        this.sendMessage('room/join-fail', {reason: '亲友圈房卡不足 无法创建房间。'}, playerRouteKey);
        return
      }
    } else {
      const fee = this.lobby.roomFee(messageBody.payload.rule)

      if (playerModel.gem < fee) {
        this.sendMessage('room/join-fail', {reason: `房卡不足请充值(个人房需要房费${fee})`}, playerRouteKey);
        return
      }
    }

    const roomId = await this.redisClient.lpopAsync('roomIds')
    if (!roomId) {
      this.sendMessage('room/join-fail', {reason: '服务器错误,无法创建房间 [-9]'}, playerRouteKey);
      return
    }


    const room = await this.lobby
      .createClubRoom(false, Number(roomId), messageBody.payload.rule, clubId, clubOwnerSocket);
    room.ownerId = messageBody.from

    try {
      const gameChannel = await this.connection.createChannel()
      const roomQueueReply = await gameChannel.assertQueue(`${this.gameName}.${room._id}`, {
        durable: false,
        autoDelete: true
      })
      await gameChannel.bindQueue(roomQueueReply.queue, 'exGameCenter', `${this.gameName}.${room._id}`)

      const roomProxy = new RoomProxy(room,
        {
          redisClient: this.redisClient, gameChannel: gameChannel, gameQueue: roomQueueReply,
          cluster: this.cluster, userCenter: {getPlayerModel: this.getPlayerModel}
        }, this.gameName)

      let theCreator = new PlayerRmqProxy(
        {...playerModel, _id: messageBody.from, ip: messageBody.ip},
        gameChannel,
        this.gameName
      )

      theCreator.sendMessage('room/join-success', {_id: room._id, rule: room.rule})
      roomProxy.joinAsCreator(theCreator)

      await this.redisClient.saddAsync('room', room._id)
      await this.roomRegister.putPlayerInGameRoom(messageBody.from, this.gameName, room._id)

      await this.redisClient.saddAsync(`cluster-${this.cluster}`, room._id)
      await this.redisClient.setAsync('room:info:' + room._id, JSON.stringify(room.toJSON()))

      // const clubInfo = await getClubInfo(clubId)

      // await requestToAllClubMember(gameChannel, 'newClubRoomCreated', clubId, this.gameName, clubInfo)
    } catch (e) {
      logger.error('create room error', e)
    }
  }

  private async recoverRooms(roomIds: string[]) {
    for (const id of roomIds) {
      try {
        const jsonString = await this.redisClient.getAsync(`room:info:${id}`)
        const roomJson = JSON.parse(jsonString)

        if (this.recoverPolicy(roomJson)) {
          const gameChannel = await this.connection.createChannel()
          const roomQueueReply = await gameChannel.assertQueue(`${this.gameName}.${roomJson._id}`, {
            durable: false,
            autoDelete: true
          })
          await gameChannel.bindQueue(roomQueueReply.queue,
            'exGameCenter',
            `${this.gameName}.${roomJson._id}`)

          const roomProxy = await RoomProxy.recover(JSON.parse(jsonString), {
            gameChannel, gameQueue: roomQueueReply,
            cluster: this.cluster, redisClient: this.redisClient, userCenter: {getPlayerModel: this.getPlayerModel}
          }, this.gameName, this.roomRecover)

          this.lobby.listenRoom(roomProxy.room)
          if (roomProxy.room.clubMode) {
            this.lobby.listenClubRoom(roomProxy.room)
          }
        }
      } catch (e) {
        logger.error('room recover failed', id, e)
      }
    }
  }
}

export class BackendProcessBuilder {
  private dataBaseUrl: string = config.get('database.url')
  private rabbitMqServer: string = 'amqp://user:password@localhost:5672'
  private gameName: string
  private cluster: string
  private recoverPolicier: (any) => boolean = alwaysOk
  private Lobby: any
  private roomRecover: recoverFunc

  connectToMongodb(mongodbUrl: string) {
    this.dataBaseUrl = mongodbUrl
    return this
  }

  connectRabbitMq(rmqServer: string) {
    this.rabbitMqServer = rmqServer
    return this
  }

  withGameName(gameName: GameNames) {
    this.gameName = gameName
    return this
  }

  withClusterName(cluster: string) {
    this.cluster = cluster
    return this
  }

  useRoomRecoverPolicy(policier: (any) => boolean) {
    this.recoverPolicier = policier
    return this
  }

  useLobby(Lobby) {
    this.Lobby = Lobby
    return this
  }

  useRecover(recover: recoverFunc) {
    this.roomRecover = recover
    return this
  }

  build(): BackendProcess {

    const rc = redis.createClient({host: config.get<string>('redis.host')}) as any
    const process = new BackendProcess({
      dataBaseUrl: this.dataBaseUrl,
      rabbitMqServer: this.rabbitMqServer,
      redisClient: rc,
      gameName: this.gameName,
      Lobby: this.Lobby,
      cluster: this.cluster,
    })
    process.recoverPolicy = this.recoverPolicier
    process.roomRecover = this.roomRecover

    return process
  }
}


const instance_id = process.env.INSTANCE_ID


if (!instance_id) {
  console.error('process.env.INSTANCE_ID must not be empty')
  process.exit(-1)
} else {
  console.log('run with instance_id id', instance_id)
}


process.on('unhandledRejection', error => {
  console.error('unhandledRejection', error.stack)
})

